import axios from 'axios';
import { v4 as uuidv4 } from 'uuid';
import { fragmentUploadPrepare, fragmentUploadSlice, fragmentUploadFinished } from '@/services/file/api';
import CryptoJS from 'crypto-js';


class FragmentUploader {
  private sliceSize: number = 1024 * 1024 * 1;
  private uploadDuration = 10000; 
  private fileMeta: API.FileMeta | null = null;
  private needUploadSlice: number[] = [];
  private retryChan: API.FilePart[] = [];
  private abortController: AbortController = new AbortController();
  private timeoutPromise!: Promise<void>;
  private timeoutId!: NodeJS.Timeout;
  private normalUploadFinished: Boolean = false;
  private _uploadedSlices: number = 0
  private onUploadProgress?: (uploadedSlices: number) => void

  get uploadSlices(): number {
    return this._uploadedSlices
  }

  set uploadSlices(value: number) {
    this._uploadedSlices = value
    if (this.onUploadProgress) {
      // this.needUploadSlice.length等于0有两种情况：数据重置时、上传完成时
      if (this.needUploadSlice.length == 0 && this.fileMeta) {
        this.onUploadProgress(100)
        return
      }
      const progress = Math.floor(value * 100 / this.needUploadSlice.length)
      this.onUploadProgress(progress)
    }
  }

  setOnUploadProgress(callback: (uploaded: number) => void): void {
    this.onUploadProgress = callback;
  }


  private async createFileMeta(file: File) {
    const md5Str = await this.calculateFileMD5(file);
    const size = file.size;

    this.fileMeta = {
      fid: uuidv4(),
      name: file.name,
      size,
      sliceNum: Math.ceil(size / this.sliceSize),
      modifyTime: new Date(file.lastModified),
      md5: md5Str,
    };
    const fileMetaStr = JSON.stringify(this.fileMeta)
    const localStoragekey = file.name + "-file_meta.json"
    localStorage.setItem(localStoragekey, fileMetaStr)
    console.log("creare fileMeta", this.fileMeta)
  }

  private async handleFileMeta(file: File): Promise<void> {
    const localStoragekey = file.name + "-file_meta.json"
    let fileMetaStr = localStorage.getItem(localStoragekey)
    if (fileMetaStr != undefined) {
      const fileMeta: API.FileMeta = JSON.parse(fileMetaStr)
      if (new Date(fileMeta.modifyTime).getTime() != file.lastModified || fileMeta.size != file.size) {
        // 文件有改动，需要重新创建
        await this.createFileMeta(file)
      } else {
        this.fileMeta = fileMeta
        console.log("exist fileMeta", this.fileMeta)
      }
    } else {
      // fileMeta不存在，创建fileMeta
      await this.createFileMeta(file)
    }
  }

  private async calculateFileMD5(file: File | Blob): Promise<string> {
    // 使用浏览器的 FileReader API 计算文件的 MD5
    const buffer = await this.readFileAsArrayBuffer(file);
    const wordArray = CryptoJS.lib.WordArray.create(buffer)
    const hash = CryptoJS.MD5(wordArray).toString()
    return hash;
  }

  private readFileAsArrayBuffer(file: File | Blob): Promise<ArrayBuffer> {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => resolve(reader.result as ArrayBuffer);
      reader.onerror = reject;
      reader.readAsArrayBuffer(file);
    });
  }

  private async handlePrepareUpload(): Promise<void> {
    if (!this.fileMeta) return;

    try {
      const { data: needUploadSlice } = await fragmentUploadPrepare(this.fileMeta)

      if (needUploadSlice.length === 0) {
        this.needUploadSlice = [];
        return;
      }

      if (needUploadSlice.length === 1 && needUploadSlice[0] === -1) {
        this.needUploadSlice = Array.from({ length: this.fileMeta.sliceNum }, (_, i) => i);
      } else {
        this.needUploadSlice = needUploadSlice;
      }
      console.log("待上传分片：", this.needUploadSlice);
    } catch (err) {
      throw new Error("读取文件待上传分片失败：", err as Error);
    }
  }

  private async uploadSlice(filePart: API.FilePart): Promise<void> {
    const formData = new FormData();
    formData.append('file', new Blob([filePart.data]), filePart.idx + "");
    formData.append('md5', filePart.md5);
    formData.append('idx', filePart.idx + "");
    formData.append('fid', filePart.fid);

    try {
      await fragmentUploadSlice(formData, {
        signal: this.abortController.signal, // 使用传入的 AbortSignal
        skipErrorHandler: true
      });
      this.uploadSlices++
      // console.log(`切片 ${filePart.idx} 上传完成`);
    } catch (error) {
      if (axios.isCancel(error)) {
        console.log(`Upload of slice ${filePart.idx} was aborted`);
      } else {
        console.error(`切片 ${filePart.idx} 上传失败:`, error);
        this.retryChan.push(filePart);
      }
    }
  }

  private async handleRetry(): Promise<void> {
    while (true) {
      try {

        if (this.retryChan.length > 0) {
          const uploadPromises = this.retryChan.map(async (filePart) => {
            console.log("重传 filePart, idx: ", filePart.idx);
            return this.uploadSlice(filePart)
          })

          this.retryChan = []
          await Promise.allSettled(uploadPromises)

          // await Promise.race([
          //   Promise.allSettled(uploadPromises),
          //   this.timeoutPromise
          // ])

          this.abortController.signal.addEventListener('abort', () => {
      
            throw (new Error('上传超时或已中止'));
          });
        }
        // 正常上传的分片全部上传完成且重传分组中分片数=0时退出
        if (this.normalUploadFinished && this.retryChan.length == 0) {
          return
        }
      } catch (err) {
        // 超时退出
        throw err
      }
    }
  }

  private async UploadSlices(file: File): Promise<void> {
    return new Promise(async (resolve, reject) => {
      const fileReader = new FileReader();
      fileReader.onload = async () => {
        try {
          const arrayBuffer = fileReader.result as ArrayBuffer;
          const data = new Uint8Array(arrayBuffer);

          const uploadPromises = this.needUploadSlice.map(async (num) => {
            const start = num * this.sliceSize;
            const end = Math.min(start + this.sliceSize, data.length);
            const sliceData = data.slice(start, end);

            const md5Str = await this.calculateFileMD5(new Blob([sliceData]));
            const filePart: API.FilePart = {
              fid: this.fileMeta!.fid,
              idx: num,
              data: sliceData,
              md5: md5Str,
            };

            return this.uploadSlice(filePart)
          })


          this.abortController.signal.addEventListener('abort', () => {
       
            reject(new Error('上传超时或已中止'));
            return
          });

          // await Promise.race([
          //   Promise.allSettled(uploadPromises),
          //   this.timeoutPromise
          // ]);

          await Promise.allSettled(uploadPromises)
          console.log("正常（上传未失败）上传分片上传完成")
          this.normalUploadFinished = true
          resolve()
        } catch (err) {
          reject(err)
        }
      };
      fileReader.onerror = (error) => console.error('读取文件失败:', error);
      fileReader.readAsArrayBuffer(file);
    })
  }

  private async handleUploadFinished(): Promise<void> {
    if (!this.fileMeta) return;

    try {
      await fragmentUploadFinished(this.fileMeta, { skipErrorHandler: true })
      const localStoragekey = this.fileMeta.name + "-file_meta.json"
      localStorage.removeItem(localStoragekey)
      console.log('上传合并完成');
    } catch (err) {
      throw err
    }
  }

  public abort() {
    this.abortController.abort();
    clearTimeout(this.timeoutId)
  }

  public reset() {
    this.uploadSlices = 0
    this.fileMeta = null
    this.needUploadSlice = []
    this.retryChan = []
    this.normalUploadFinished = false
    this.abortController = new AbortController();
  }



  public async HandleUpload(file: File): Promise<void> {
   // 启动定时机制，超时上传失败
   this.timeoutId = setTimeout(() => {
    this.abortController.abort();
  }, this.uploadDuration);


    // 获取文件meta信息
    await this.handleFileMeta(file);
    // 准备上传文件，确认需要上传的分片
    await this.handlePrepareUpload();

    // 上传分片
    if (this.needUploadSlice.length !== 0) {
      await this.UploadSlices(file);
      // 处理失败重传分片
      // todo: 失败重传没有和上传分片并发执行
      await this.handleRetry()
    }
    clearTimeout(this.timeoutId)


    await this.handleUploadFinished();
  }
}

export default FragmentUploader;